Многопоточное программирование в Java - Тимур Машнин
Шрифт:
Интервал:
Закладка:
Этот класс используется в ситуациях, когда добавлять числа приходится гораздо чаще, чем запрашивать результат.
И несложно догадаться, что, давая прирост в производительности, LongAdder требует гораздо большего количества памяти из-за того, что он хранит все слагаемые.
Классы LongAccumulator и DoubleAccumulator можно использовать для накопления результатов в соответствии с предоставленным LongBinaryOperator и DoubleBinaryOperator.
То есть вместо простого сложения класс обрабатывает входящие значения с помощью лямбды типа LongBinaryOperator или DoubleBinaryOperator, которая передаётся при инициализации.
Так же, как и Adder, Accumulator хранит весь набор переданных значений в памяти, уменьшая взаимодействие между потоками.
Важно помнить, что Accumulator будет работать правильно, если мы снабдим его коммутативной функцией, где порядок накопления не имеет значения.
В этом примере мы создаем LongAccumulator, который добавляет новое значение к значению, которое уже было в накопителе.
При передаче числа в качестве аргумента методу accumulate, этот метод вызовет функцию sum.
ThreadLocal
Предположим, что у нас есть веб приложение, которое состоит из многих компонентов, и нам в любой точке кода может понадобится информация о пользователе, от которого пришел http запрос.
Причем не в каждой точке кода будет доступ к Http сессии, поэтому не везде можно будет узнать от какого пользователя пришел запрос.
Встает вопрос, что делать.
Добавлять в каждый метод каждого класса дополнительный параметр, представляющий данные пользователя?
Эту проблему можно решить с помощью переменной ThreadLocal.
С использованием ThreadLocal мы можем привязать данные о пользователе к потоку обработки http запроса, и достать эту информацию в любом месте программы.
ThreadLocal можно рассматривать как область доступа потока, например, область запроса или область сеанса.
Вы можете установить любой объект в ThreadLocal, и этот объект будет своим для каждого потока.
К значениям, хранящимся в ThreadLocal, можно получить доступ из любой точки внутри этого потока.
Если поток вызывает методы из нескольких классов, то все методы могут видеть переменную ThreadLocal, заданную другими методами, потому что они выполняются в одном потоке.
При этом значения, хранящиеся в Thread Local, являются уникальными для потока, то есть каждый поток будет иметь собственную переменную Thread.
Один поток не может получить доступ и изменить переменную ThreadLocal другого потока.
ExecutorService и пул потоков
До сих пор мы создавали и управляли потоками вручную.
Однако, когда в приложении вам нужно создать множество задач, которые будут выполняться во множестве потоков, или нужно создать множество задач, которые будут выполняться по очереди в одном потоке, возникают проблемы.
Например, у вас есть веб-приложение, которое помимо прочей обработки пользовательского запроса, в фоновом потоке посылает email.
Потоки, используемые для обработки запросов, поступающих на сервер приложений, создаются и управляются самим контейнером сервера. Вам не нужно об этом беспокоиться.
Однако, если вашему приложению необходимо обрабатывать долгие задачи, инициированные пользовательскими запросами, и вы хотите быстро отвечать на запросы, вы можете использовать отдельные потоки для этих задач.
Самым простым решением, было бы создание нового потока Thread каждый раз, когда поступает запрос пользователя, и обслуживать этот запрос в новом потоке.
Однако при этом накладные расходы на создание нового потока для каждого запроса являются значительными.
Сервер, создавая новый поток для каждого запроса, будет тратить дополнительное время и будет потреблять дополнительные системные ресурсы, создавая и уничтожая потоки.
В дополнение к накладным расходам на создание и уничтожение потоков, активные потоки потребляют системные ресурсы.
Создание слишком большого количества потоков в одной JVM может привести к тому, что система исчерпает память из-за чрезмерного потребления памяти.
Чтобы предотвратить переполнение ресурсов, серверные приложения нуждаются в ограничении количества запросов, обрабатываемых в любой момент времени.
Решить все эти проблемы поможет программный интерфейс Java Concurrency API, который предлагает интерфейсы для запуска и управления жизненным циклом задач, для планирования выполнения задач, обеспечивая создание очереди задач и пула потоков.
Executor — это простой интерфейс, содержащий метод execute для запуска задачи, заданной объектом Runnable.
ExecutorService — расширяет интерфейс Executor, добавляя функции для управления жизненным циклом задач.
ExecutorService также предоставляет метод submit, который может принимать объект Runnable, а также объекты Callable, которые позволяют задаче вернуть значение.
ScheduledExecutorService — расширяет интерфейс ExecutorService, добавляя функциональность для планирования выполнения задач.
Помимо вышеуказанных трех интерфейсов, Concurrency API также предоставляет класс Executors, который содержит фабричные методы для создания различных видов ExecutorService.
Под капотом, при использовании Executor интерфейсов, задачи Runnable сначала помещаются в очередь, а затем их выполнение распределяется по рабочим потокам пула потоков Thread Pool.
Пул потоков состоит из рабочих потоков, которые существуют отдельно от выполняемых задач и могут использоваться для выполнения нескольких задач.
Существует несколько типов пулов потоков.
Это фиксированный пул потоков, который содержит определенное количество потоков.
И если поток каким-то образом завершается во время своего использования, он автоматически заменяется новым потоком.
Задачи отправляются в пул потоков через внутреннюю очередь, которая содержит задачи, если количество задач больше, чем размер пула потоков.
Далее есть кэшированный пул потоков, который создает новые потоки по мере необходимости, но будет использовать ранее созданные потоки, когда они будут доступны.
И есть пул с одним потоком и неограниченной очередью.
Если этот единственный поток завершается из-за сбоя во время выполнения, на его место создается новый поток.
Задачи выполняются последовательно, и только одна задача будет активна в любое время.
Классы ThreadPoolExecutor и ScheduledThreadPoolExecutor позволяют установить свои собственные настройки для объекта Executor и определить основной размер пула потоков, максимальный размер пула потоков, указать тип используемой очереди и другое.
При этом если пул потоков не достиг еще своего основного размера, он создает новые потоки.
Если основной размер достигнут и нет простаивающих потоков, задачи ставятся в очередь.
Если основной размер достигнут, нет простаивающих потоков, и очередь заполнена, создаются новые потоки (пока не будет достигнут максимальный размер).
Если достигнут максимальный размер, нет простаивающих потоков, и очередь заполнена, новые задачи отклоняются.
Таким образом, пул потоков предлагает решение проблемы с накладными расходами на создание и уничтожение потоков, и проблемы чрезмерного потребления ресурсов при создании слишком большого количества потоков.
Пул потоков